Emit the LM head weight gradient in kernel order under explicit sharding - #5091
Emit the LM head weight gradient in kernel order under explicit sharding#5091NuojCheng wants to merge 9 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a detailed performance analysis guide comparing explicit and automatic sharding modes, and implements a performance optimization via the lm_head_kernel_transposed configuration. This option allows storing the untied LM head kernel as [vocab, embed] instead of [embed, vocab], which prevents an expensive transpose operation under explicit sharding. The changes update DenseGeneral to support the transposed kernel layout, add corresponding configuration validation, adjust integration mappings for Tunix and vLLM, and include comprehensive unit tests. The single review comment regarding a date typo in the documentation has been filtered out, leaving no further feedback to provide.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Under `shard_mode: explicit`, JAX's `lower_with_sharding_in_types` annotates
every sharding-in-types primitive's output with a `custom_call_target="Sharding"`
op. One of those lands between the LM head weight-gradient `dot` and the
`transpose` that follows it, and XLA's algebraic simplifier will not fold
`transpose(dot(A, B))` into `dot(B, A)` across it. Census on the dp4 module:
auto has 17 adjacent (foldable) transpose-of-dot pairs, explicit has 0. The
transpose survives to codegen, which costs twice -- the gradient is left sharded
on its minor-most dimension, below the 512-byte extent
`tpu-all-reduce-scatter-fusion` requires, so the weight-gradient all-reduce is
never rewritten into a reduce-scatter; and the resulting layout propagates
through the Adam update, forcing six relayout copies on the entry parameters.
Remove the need for the fold rather than the barrier. `DenseGeneral` gains a
`weight_grad_in_kernel_order` option backed by a `custom_vjp` whose backward pass
contracts `dk` straight into the kernel's stored axis order, so no transpose is
emitted at all. The stored kernel, its sharding and its initialization are
untouched, so this is checkpoint-compatible; the gradient differs from the
default rule only by floating-point reassociation. It self-disables under
`shard_mode: auto`, where XLA folds the transpose on its own, and under
quantization, where it cannot own the dot. The untied LM head opts in via
`lm_head_weight_grad_in_kernel_order` (default false) on both the linen and nnx
decoder paths.
Measured on v5p, 4 chips, xprof `train_step_ns` median on `/device:TPU:0`,
56 runs, arms run back-to-back per config so within-row deltas carry no session
drift:
config auto us explicit +kernel-order B-A% D-A% rep spread
deepseek 9081.0 9883.5 9216.8 +8.84 +1.50 0.56%
llama2 4027.4 4223.8 3995.3 +4.88 -0.80 0.79%
mistral 3905.2 4084.6 3853.0 +4.59 -1.34 0.97%
mixtral 10104.5 10407.2 10129.8 +3.00 +0.25 0.12%
qwen3 8261.8 8608.6 8214.8 +4.20 -0.57 0.51%
w2048 14972.1 15215.1 14886.3 +1.62 -0.57 0.04%
prod 196039.4 196558.5 196101.9 +0.26 +0.03 0.03%
Read the two halves differently. The five emb-512 rows go from a +4.59% median
penalty to -0.57%, but emb 512 / fsdp 4 puts 256 bytes per shard on the
minor-most dimension, below XLA's 512-byte AR->RS gate -- so those rows are a
benchmark artifact of the shrunk proxies and no production job has that geometry.
The ship criterion is the two emb-2048 rows, where the bar is no regression
rather than a win: prod +0.03% against a 0.03% rep spread, w2048 -0.57%.
Acceptance: on llama2/shrink/fsdp4 the explicit dump gains
`%all-reduce-scatter.2`, byte-for-byte identical to auto's
`%all-reduce-scatter.5` (`bf16[512,32000] -> bf16[128,32000]`, layout
`{1,0:T(8,128)(2,1)}`, dim-0 dynamic-slice), and `f32[emb/n, 32000]` relayout
copies in `after_optimizations` drop from 6 to 0.
Numerical equivalence over 20 steps against auto, max absolute loss deviation:
llama2 explicit 2.62e-04, with the flag 2.52e-04; mixtral 1.41e-04 and 9.63e-05.
The flag sits at explicit's own noise level, as reassociation alone predicts.
`logits_via_embedding` is rejected rather than silently ignored, since the flag
only applies to the untied head.
An earlier revision of this branch shipped `lm_head_kernel_transposed`, which
stores the kernel as `[vocab, embed]` to reach the same layout. It is dropped
here: it is 1.85-4.02% slower than the custom_vjp on four of seven configs
because it recovers only the relayout copies and not the reduce-scatter, it has
no checkpoint-conversion path, and at the flipped shape `jax.random` draws a
different matrix from the same seed, which perturbs the loss ~35x more than
explicit's own noise (llama2 9.42e-03 vs 2.52e-04).
Analysis, including the measured negative result for eliding redundant reshard
barriers, is in docs/guides/optimization/shard_mode_performance.md.
6ff6d3c to
780e3e4
Compare
`attend_on_embedding` materialized `table.T` to compute the tied output head's logits. Under `shard_mode: explicit` the transposed table is a distinct typed value, so XLA cannot fold it into the dot's dimension numbers: the table shard is cast to bf16 once for the input lookup in `Embed.__call__` and a second time here, and the tied head's weight gradient comes out flipped and needs a transpose to put back. Expressing the transpose as dimension numbers via einsum keeps both consumers on one cast and leaves the gradient in the table's own axis order. Under `shard_mode: auto` XLA already folded the `.T` away, so this is a no-op there. It reassociates the accumulation, so logits agree to float rounding rather than bit for bit.
`YarnRotaryEmbedding.freqs_cis` built the whole `[max_position_embeddings, half_dim]` table and then indexed `max_target_length` rows out of it. The table is traced, not a constant XLA can hoist, so it was rebuilt inside every layer of every step: on deepseek2-16b that is `f32[163840, 32]` per layer per step, of which 0.6% is read. Row `p` is `exp(1j * p * corrected_freqs)`, so the rows can be produced straight from `position`. `freqs_cis_at` does that; `freqs_cis` stays as the reference definition the new test checks against, and the two agree bit for bit. Measured on deepseek at emb 2048 / mlp 8192 / L8 this is -1.4% of step time, and the same -1.4% under `shard_mode: auto` and `explicit` alike. `_cis` writes `exp(1j * theta)` as `cos(theta) + 1j*sin(theta)`. It is bit-identical, but it avoids XLA's overflow-safe complex `exponential` expansion, which evaluates `exp(real(1j*theta)) == exp(0)` over the whole tensor -- twice under explicit sharding, where the `Sharding` custom-call on the broadcast `1j` blocks the reassociation that would let CSE merge them.
The in-loop counterpart of `lm_head_weight_grad_in_kernel_order`: wire the same `custom_vjp` to the three `MlpBlock` projections, the four attention projections and the nine MLA / Indexer projections, so their weight gradients are contracted straight into each kernel's stored axis order. Unlike the LM-head flag this is a trade, not a fix, so it defaults off. Under `shard_mode: explicit` the `Sharding` custom-calls decouple the gradient's layout from the stored parameter's, leaving XLA free to pick it; the flag declines that freedom and reproduces `shard_mode: auto`'s layout op for op. Whether that is a win depends on the scanned gradient stack's depth. At emb 2048 / mlp 8192 it is worth 0.90pp on gemma3-4b and 0.16pp on deepseek3-16b, whose stacks are 3 and 1 deep, and costs 0.06%-0.60% on the six models whose stacks are 16 deep -- two points below the line against six above is not enough to fit a default on, so it ships off with the two models named in the docs. `_permuted_sharding` handles the >2-D kernels the in-loop sites have; on a 2-D kernel it degenerates to the identity, which is why the LM head never needed it. Stored kernels and their initialization are untouched, so no checkpoint conversion is needed, and the flag is inert under `shard_mode: auto` and on quantized layers. See docs/guides/optimization/shard_mode_performance.md section 4.8.
Re-measured across all eight onboarded models at emb 2048 / mlp 8192 / 16 layers -- above the geometry gate an emb-512 proxy sits below -- the flag has never lost to `shard_mode: auto`: on any model, at any depth (L12/L14/L16), at any vocab (4096 or 32000). Without it, explicit is +3.25% on qwen3-8b and +0.92% on deepseek3-16b. There is no configuration measured where leaving it off is the better choice, so off is the wrong default. It is now tri-state. `None` resolves to on for an untied head under `shard_mode: explicit` and off everywhere else, which is exactly where it can do something: under `auto` XLA folds the transpose itself, and a tied head has no kernel of its own. An explicit `true` on a tied head is now an error rather than silently ignored. With this default and nothing else in the config but `shard_mode: explicit`, the median across the eight models is -0.330% and the worst case is gemma3 at +0.803%; adding `dense_weight_grad_in_kernel_order` on the two models that want it leaves no model regressing by more than 0.041%.
Re-measures the whole matrix at above-gate geometry with nothing written out but `shard_mode` (section 5.4, the table to quote), documents the LM-head flag's new default, adds section 4.8 on what the dense-layer flag actually trades, section 5.5 on the YaRN table, and section 6.3 on the rotated reduce-scatter -- explicit's largest and most stable win, 480 of 495 `collective-permute-start` ops deleted on every untied model. Also records what did not work: thirteen candidates that were implemented or measured and refuted, and a systematic hunt for any remaining explicit-only cost on llama2/mistral/qwen3 that found nothing above ~0.10% of step.
The hand-written custom_vjp reordered the weight-gradient dot's operands itself and permuted the result back, to work around the sharding barrier JAX puts on every primitive result under an explicit mesh. Wrapping the forward dot in an auto_axes region instead removes the barrier for the length of that one dot, so Shardy re-propagates the operands' shardings exactly as shard_mode: auto would and XLA performs the fold on its own. Same mechanism, 114 fewer lines, and gradients are now bit-identical to the default rule rather than agreeing to rounding. Step times agree with the custom_vjp to within 0.15 pp on all five models measured both ways. Outside a mesh there is no barrier to remove, so eval_shape reports no sharding and the region is skipped.
The flag shipped default-off because its sign looked model-dependent: worth ~0.9 pp on gemma3 and ~0.16 pp on deepseek, and a 0.06-0.60% cost on the other six models. Every one of those eight points was measured at 16 layers, which is a scan depth where an unrelated XLA layout pathology inflates both sharding modes by ~25% and swamps the effect being read. Re-measured at depths where that pathology does not fire, the flag has one sign everywhere and the two flags are additive. Across 15 (model, depth) pairs the LM-head flag alone leaves a median +0.463% penalty against auto, worst +1.038%; with this one on as well the median is +0.005% and the full range is -0.183% to +0.134%. On the eight-model matrix at healthy depth, explicit with nothing written out but shard_mode now reads median +0.000% against auto, worst +0.133%, and wins outright on gemma-2b (-0.685%) and gemma3-4b (-0.374%). Also rewrites both flags' descriptions around the auto_axes region that now implements them.
Walking llama2 from 2 to 24 layers, ns-per-layer is flat at ~3.3 M
except at stack lengths 2, 4, 8, 16 and 24, where it jumps 25-34%. At
those lengths XLA assigns layout {2,0,1} to the scanned parameter stack,
so each dynamic-update-slice writes a degenerate T(1,128)-tiled slice:
8.16 ms of a 36.5 ms step at L8, identical in both sharding modes.
param_scan_axis: 0 recovers it (-23.6% at L8, -24.9% at L16) and it
survives at production geometry.
Every benchmark in the guide before this revision used a stack length of
4, 8 or 16, so several "explicit wins" rows are noise inside a 25%
inflation. The originals are kept and marked; new section 4.9
characterizes the pathology and new section 5.6 replaces section 5.4
with the eight-model matrix re-measured on the shipped defaults at
depths where it does not fire.
What
Under
shard_mode: explicit, MaxText pays a step-time penalty that traces to a single unfoldable transpose on the LM head's weight gradient. This addslm_head_weight_grad_in_kernel_order(default off), which removes it, and drops the previously-proposedlm_head_kernel_transposedin favour of it.Why
JAX's
lower_with_sharding_in_typesannotates every sharding-in-types primitive's output with acustom_call_target="Sharding"op — 65 of them underautoon llama2 versus 1,276 underexplicit. One lands between the LM head weight-gradientdotand thetransposethat follows it, and XLA's algebraic simplifier will not foldtranspose(dot(A,B)) → dot(B,A)across it. Census on the dp4 module: auto has 17 adjacent (foldable) transpose-of-dot pairs, explicit has 0.The transpose survives to codegen and costs twice:
tpu-all-reduce-scatter-fusionrequires, so the weight-gradient all-reduce is never rewritten into a reduce-scatter — 2× the wire bytes for that tensor.The barrier itself cannot be removed: it is JAX's, not MaxText's. Gating MaxText's
out_shardingpin produces a 0-line HLO diff.How
DenseGeneralgainsweight_grad_in_kernel_order, backed by acustom_vjpwhose backward pass contractsdkstraight into the kernel's stored axis order — so there is no transpose to fold in the first place. The stored kernel, its sharding and its initialization are untouched, which makes this checkpoint-compatible; the gradient differs from the default rule only by floating-point reassociation.It self-disables under
shard_mode: auto(where XLA folds the transpose on its own) and under quantization (where it cannot own the dot).logits_via_embeddingis rejected rather than silently ignored. The untied LM head opts in vialm_head_weight_grad_in_kernel_orderon both the linen and nnx decoder paths.Measured
v5p, 4 chips, xprof
train_step_nsmedian on/device:TPU:0, 56 runs. All arms run back-to-back per config so within-row deltas carry no session drift.Please read the two halves of that table differently — this is the part that is easy to over-claim.
The five
emb 512rows go from a +4.59% median penalty to −0.57%, beatingautooutright on three of five. Butemb 512 / fsdp 4puts 256 bytes per shard on the minor-most dimension, below XLA's 512-byte AR→RS gate, so mechanism (1) is active only there. No production job has that geometry — those rows are an artifact of the shrunk benchmark proxies.The two bold rows are the real ship criterion, and the bar there is no regression, not a win:
prod(emb 2048, 16 layers, seq 4k) is +0.03% against a 0.03% rep spread,w2048is −0.57%. There was almost nothing left to recover at that geometry, and recovering it costs nothing.Acceptance
On llama2/shrink/fsdp4 the explicit dump gains
%all-reduce-scatter.2, byte-for-byte identical to auto's%all-reduce-scatter.5—bf16[512,32000] → bf16[128,32000], layout{1,0:T(8,128)(2,1)}, dim-0 dynamic-slice.f32[emb/n, 32000]relayout copies inafter_optimizationsdrop from 6 to 0, matching auto.Numerical equivalence
20 training steps against the
autobaseline, same seed, max absolute loss deviation:The flag sits at explicit's own noise level — on mixtral slightly below it — which is what reassociation alone predicts.
Changed since the first revision of this PR
lm_head_kernel_transposedhas been removed. That flag stored the kernel as[vocab, embed]to reach the same layout, and it did work. It is dropped because thecustom_vjpdominates it on every axis measured:params-decoder-logits_dense-kernel, so it cannot be flipped on an existing run.jax.randomdraws a different matrix from the same seed, so it perturbs the loss ~35× more than explicit's own noise (llama2 9.42e-03 vs 2.52e-04) — reinitialization, not rounding.Shipping a strictly-dominated flag that also carries a silent checkpoint hazard seemed worse than shipping one flag that works, so the transposed-kernel code, its config plumbing, its
integration/tunixand vLLM converter changes, and its tests are all gone. Happy to restore it if you would rather have both.A second negative result is documented rather than shipped. Eliding redundant
reshardbarriers (no-op reshards where the aval already carries the requested sharding) looked like the obvious companion fix. It was implemented and measured: it removes ~4% of barriers in a real module and the optimized executable is op-identical (llama2 2889→2889, prod 2942→2942, deepseek 9642→9642, zero differing ops). XLA already discards no-opShardingcustom-calls. Recorded in the doc so the next person does not spend the week.Testing
tests/unit/linears_test.py— 7 new tests: kernel bit-identity, forward equality, gradient equality across three contraction shapes, the LM-head 2-D shape in fp32 and bf16, composition withnnx.remat, the auto-mode no-op, and the quantization yield. Whole file passes (21 passed, 1 TPU-only skipped).tests/unit/pyconfig_test.py,tests/unit/configs_value_test.py— pass.pyinkclean,pylintclean (the two remaining findings intypes.pypre-exist onmain).Notes
false. The flag is inert underautoand checkpoint-safe underexplicit, so flipping the default is a follow-up decision, not a prerequisite.docs/guides/optimization/shard_mode_performance.md.custom_vjpto the in-loopDenseGenerals, gated on a leading FSDP axis. Estimated +360.7 µs atprodgeometry, but it is untested undernnx.scan+ remat and is roughly a week of work.Follow-up (2026-09-03)
Four commits pushed on top of the above. Two of them close the last items this PR left open — the default, and the in-loop
DenseGenerals — and two are separate wins found while measuring. Both "Notes" bullets above are superseded: the default is now on, and the in-loop lever is in this PR.The one table
Eight onboarded models at emb 2048 / mlp 8192 / 16 layers (deepseek 8, gemma3 18 — it scans in groups of 6), seq 1024, pdbs 1, fsdp 4 on v5p-8. One build, 54 runs, 3 reps per arm, medians. The
explicitcolumn has nothing in the config butshard_mode: explicit— it is what a user gets.autonsexplicitnsmistral-7bqwen3-8bllama2-7bgemma2-2bgemma-2bmixtral-8x7bdeepseek3-16b(L8)gemma3-4b(L18)Median −0.330%, faster on five of eight and inside the rep spread on a sixth. With the two per-model dense-flag settings applied, the worst regression anywhere is mixtral's +0.041%, which is inside mixtral's own 0.07% rep spread. Before this work the worst cases were qwen3 +3.25%, gemma3 +2.02% and deepseek +0.92%.
lm_head_weight_grad_in_kernel_orderis now on by default where it can actRe-measured on every model, at L12/L14/L16, and at vocab 4096 and 32000, the flag has never lost to
auto— there is no measured configuration where off is the better choice, so off was the wrong default.explicitexplicit+ flagMixtral is the one model the flag does not help; its −1.393% is an MoE-scan scheduling windfall unrelated to the LM head (it oscillates +0.120% / −0.151% / −0.038% across L12/L14/L16, and shrinking the vocab 8× leaves the spread unchanged), and the flag costs it nothing — it lands on
auto's schedule rather than losing to it.The flag is therefore tri-state:
Noneresolves to on for an untied head undershard_mode: explicit, off everywhere else. An explicittrueon a tied head is now an error rather than silently ignored.dense_weight_grad_in_kernel_order— the in-loop lever, shipped default-offThe same
custom_vjpwired to the threeMlpBlock, four attention and nine MLA/Indexer projections. It turned out not to be a one-signed missed optimization. Under explicit theShardingcustom-calls decouple the gradient's layout from the stored parameter's, so XLA picks the dot's preferred layout; the flag declines that freedom and reproducesauto's layout op for op. On the deep-scan models both modes relayout anyway and explicit's source is 24% cheaper to convert (20.2 vs 26.4 µs/copy), so the flag gives back a real win:The discriminator is the scanned gradient stack's depth — the two winners are 3 and 1 deep, the six losers all 16 — but two points below the line against six above is not enough to fit a default on, so it ships off, with the two models named in
base.ymland in section 4.8 of the doc. Stored kernels and their initialization are untouched, so there is no checkpoint hazard, and it is inert underautoand on quantized layers.Two wins that are not about sharding at all
YarnRotaryEmbeddingbuilt its whole frequency table inside every layer of every step to readmax_target_lengthrows out of it —f32[163840, 32]on deepseek, 0.6% of it used. Computing the rows straight frompositionis −1.4% of step time, equally in both modes, which is the check that it is not a sharding effect. It survived a full mode-comparison sweep precisely because both modes paid it: A/B diffing two modes cannot find work both modes do. The detector that found it — flag any op whose output element count is ≥ 8× its largest input's — was then run over all eight models and found nothing else above ~0.06%.attend_on_embeddingmaterializedtable.T. Under explicit that is a distinct typed value XLA cannot fold into the dot's dimension numbers, so the table shard was cast to bf16 twice and the tied head's gradient came out flipped. Expressed as einsum dimension numbers instead.Also in the doc, not in the code
bf16[24,8192] collective-permute-startops — the rotated-reduce-scatter fixup — on every untied model.Testing (follow-up commits)
tests/unit/linears_test.py,tests/unit/embeddings_test.py,tests/unit/pyconfig_test.py— 72 passed, 1 TPU-only skipped, 26 subtests. New coverage: thecustom_vjpon >2-D kernels via_permuted_sharding,freqs_cis_atbit-identical to indexing the full table at the bottom/middle/top of a 163,840-row table,attend_on_embeddingforward and gradient equality across three query ranks, and the tri-state resolution including the tied-head rejection.tests/unit/configs_test.py,tests/unit/configs_value_test.py— 113 passed, 110 subtests.pyink,pylint,codespell,mdformat,yamllintclean via pre-commit (the twotypes.pypylint findings pre-exist onmain).Second follow-up (2026-09-03): the depth sweep that invalidated the tables above
Three commits:
5b9725702,6a54684a7,169aac772.Everything above this line was measured at 4, 8 or 16 decoder layers. All three are scan depths where an unrelated XLA layout pathology inflates both sharding modes by ~25%, and a 25% inflation is more than enough scheduling noise to swamp — and repeatedly flip the sign of — a 0.4% sharding effect. The tables are kept and marked rather than deleted, because the corrections reverse conclusions that looked solid.
The pathology (doc section 4.9)
Walking llama2 from 2 to 24 layers at emb 2048 / mlp 8192, ns-per-layer is flat at ~3.3 M except at stack lengths 2, 4, 8, 16 and 24, where it jumps 25–34%:
autonsautonsAt those lengths XLA assigns layout
{2,0,1}to the scanned parameter stackf32[L,512,8192]instead of{2,1,0}, so everydynamic-update-slicewrites a degenerateT(1,128)-tiled slice — three op families costing 8.16 ms of a 36.5 ms step at L8, byte-identical in both sharding modes.param_scan_axis: 0removes it (L8 −23.6%, L16 −24.9%) and costs 0.18–0.28% at healthy depths; it survives at production geometry (+14.1% at L8, +11.5% at L16). This is unrelated toshard_mode, is larger than everything else in the doc, and is not changed by this PR — it is documented as section 8 item 03 for someone to take on separately.What that does to the two flags
Re-measured where the pathology does not fire,
dense_weight_grad_in_kernel_orderhas one sign everywhere and the two flags are additive. llama2 / qwen3 at 18 layers, four ways:Across 15 (model, depth) pairs the LM-head flag alone leaves a median +0.463% penalty against
auto(worst +1.038%, gemma2); with both flags the median is +0.005% and the full range is −0.183% … +0.134%. The "two models only" recommendation in the section above was fitted entirely to contaminated points, sodense_weight_grad_in_kernel_ordernow defaults on undershard_mode: explicit, same tri-state shape as the LM-head flag.The replacement table (doc section 5.6)
Eight models,
d16geometry, 3 reps, nothing in the config butshard_mode: explicit— both flags resolve themselves. Every depth chosen so the pathology is not firing:autonsexplicitnsgemma-2bgemma3-4bmixtral-8x7bllama2-7bgemma2-2bmistral-7bqwen3-8bdeepseek2-16bMedian +0.000%; worst case +0.133% against a 0.16% rep spread on the same row. Six of eight rows sit inside their own rep spread in both directions; the two that do not are
gemma-2bandgemma3-4b, and explicit wins both.The three models this round set out to fix, before → after:
gemma3-4bmixtral-8x7bdeepseek2-16bgemma3 is where the dense flag does real work: 1.18 pp, and it changes the sign. mixtral and deepseek were already inside noise and still are; what the flag buys there is not a win but the absence of a tail.
mixtral and deepseek are at 14 and 12 layers because at 18 both exceed HBM at this geometry (deepseek asks 43.56 G of temporaries against 31.24 G available). Both are healthy stack lengths, which is the property that matters.
custom_vjp→jax.sharding.auto_axesBoth flags are now implemented by tracing the forward dot inside a
jax.sharding.auto_axesregion rather than by hand-writing the reordered gradient. The region drops the sharding barrier for the length of that one dot, so Shardy re-propagates the operands' shardings exactly asshard_mode: autowould and XLA performs the fold itself:114 fewer lines, gradients now bit-identical to the default rule rather than agreeing to rounding, no operand-permutation bookkeeping for >2-D kernels, and it works at all 17 call sites including MLA. Step times agree with the
custom_vjpto within 0.15 pp on all five models measured both ways.The unit test for the mechanism changed accordingly:
auto_axesleaves the transpose in the jaxpr (XLA folds it later), so the test now asserts the barrier is gone — under explicit the weight-gradient transpose's operand is defined bystablehlo.dot_generalwith the flag on and bysdy.sharding_constraintwith it off.Disclosed, not fixed
lm_head_weight_grad_in_kernel_order: falseis called out in the doc for mixtral users who can measure it.param_scan_axis: 0is not made a default here. It is worth ~25% at the affected depths but costs 0.18–0.28% elsewhere and has only been characterized on one model family and one chip generation.Testing
tests/unit/linears_test.py,tests/unit/pyconfig_test.py— 60 passed, 1 TPU-only skipped, 20 subtests.tests/unit/sharding_test.py,sharding_nnx_test.py,sharding_compare_test.py,sharding_desc_test.py,moe_test.py,attention_test.py— 190 passed, 126 skipped.pyink,pylint(10.00/10 on both changed Python files),codespell,mdformat,yamllintclean via pre-commit.